/* DateType.js
 *
 * Copyright 2006, Tim Dwelle.
 *
 * Licensed under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in
 * compliance with the License.  You may obtain a copy of
 * the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in
 * writing, software distributed under the License is
 * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
 * CONDITIONS OF ANY KIND, either express or implied.  See
 * the License for the specific language governing
 * permissions and limitations under the License.
 *
 */

$package("dowry.datatype");

/**
 * Object representation of a date datatype.
 *
 */
$class("DateType").$extends("Datatype").$as(
{
    /**
     * Constant for the string representations of AM and
     * PM.
     *
     */
    AM_PM : [ Dowry.msgs.getMessage("am"),
              Dowry.msgs.getMessage("pm") ],

    /**
     * Constant for the string representations of the days
     * of the week (beginning with Sunday).
     *
     */
    DAYS_OF_WEEK : [ Dowry.msgs.getMessage("Sunday"),
                     Dowry.msgs.getMessage("Monday"),
                     Dowry.msgs.getMessage("Tuesday"),
                     Dowry.msgs.getMessage("Wednesday"),
                     Dowry.msgs.getMessage("Thursday"),
                     Dowry.msgs.getMessage("Friday"),
                     Dowry.msgs.getMessage("Saturday") ],

    /**
     * Constant for the string representations of the days
     * of the week in abbreviated form (beginning with
     * Sunday).
     *
     */
    DAYS_OF_WEEK_SHORT: [ Dowry.msgs.getMessage("Sun"),
                          Dowry.msgs.getMessage("Mon"),
                          Dowry.msgs.getMessage("Tue"),
                          Dowry.msgs.getMessage("Wed"),
                          Dowry.msgs.getMessage("Thu"),
                          Dowry.msgs.getMessage("Fri"),
                          Dowry.msgs.getMessage("Sat") ],

    /**
     * Constant for the error message when validation finds
     * value that does not represent a date.
     *
     */
    ERROR_INVALID : Dowry.msgs.getMessage("ERROR_INVALID"),

    /**
     * Constant for the default date format.
     *
     */
    FORMAT_DEFAULT : Dowry.msgs.getMessage("default"),

    /**
     * Constant for the short date format.
     *
     */
    FORMAT_SHORT : Dowry.msgs.getMessage("short"),

    /**
     * Constant for the medium date format.
     *
     */
    FORMAT_MEDIUM : Dowry.msgs.getMessage("medium"),

    /**
     * Constant for the long date format.
     *
     */
    FORMAT_LONG : Dowry.msgs.getMessage("long"),

    /**
     * Constant for the full date format.
     *
     */
    FORMAT_FULL : Dowry.msgs.getMessage("full"),

    /**
     * Constant for the parse/format mask to use with the
     * full date format.
     *
     */
    FULL : Dowry.msgs.getMessage("dateFullPattern"),

    /**
     * Constant for the parse/format mask to use with the
     * long date format.
     *
     */
    LONG : Dowry.msgs.getMessage("dateLongPatern"),

    /**
     * Constant for the parse/format mask to use with the
     * medium date format.
     *
     */
    MEDIUM : Dowry.msgs.getMessage("dateMediumPattern"),

    /**
     * Constant for the string representations of the months
     * (beginning with January).
     *
     */
    MONTHS : [ Dowry.msgs.getMessage("January"),
               Dowry.msgs.getMessage("February"),
               Dowry.msgs.getMessage("March"),
               Dowry.msgs.getMessage("April"),
               Dowry.msgs.getMessage("May"),
               Dowry.msgs.getMessage("June"),
               Dowry.msgs.getMessage("July"),
               Dowry.msgs.getMessage("August"),
               Dowry.msgs.getMessage("September"),
               Dowry.msgs.getMessage("October"),
               Dowry.msgs.getMessage("November"),
               Dowry.msgs.getMessage("December") ],

    /**
     * Constant for the string representations of the months
     * in abbreviated form (beginning with January).
     *
     */
    MONTHS_SHORT : [ Dowry.msgs.getMessage("Jan"),
                     Dowry.msgs.getMessage("Feb"),
                     Dowry.msgs.getMessage("Mar"),
                     Dowry.msgs.getMessage("Apr"),
                     Dowry.msgs.getMessage("May"),
                     Dowry.msgs.getMessage("Jun"),
                     Dowry.msgs.getMessage("Jul"),
                     Dowry.msgs.getMessage("Aug"),
                     Dowry.msgs.getMessage("Sep"),
                     Dowry.msgs.getMessage("Oct"),
                     Dowry.msgs.getMessage("Nov"),
                     Dowry.msgs.getMessage("Dec") ],

    /**
     * Constant for the parse/format mask to use with the
     * short date format.
     *
     */
    SHORT : Dowry.msgs.getMessage("dateShortPattern"),

    /**
     * Constant for the parse/format token for am/pm
     * (default is '%am').
     *
     */
    TOKEN_AM : "%am",

    /**
     * Constant for the parse/format token for day in month
     * (default is '%d').
     *
     */
    TOKEN_D : "%d",

    /**
     * Constant for the parse/format token for day of week
     * (default is '%dayOfWeek').
     *
     */
    TOKEN_DAY_OF_WEEK : "%dayOfWeek",

    /**
     * Constant for the parse/format token for day in month
     * padded to 2 digits (default is '%dd').
     *
     */
    TOKEN_DD : "%dd",

    /**
     * Constant for the parse/format token for day of week
     * as an abbreviated string (default is '%dow').
     *
     */
    TOKEN_DOW : "%dow",

    /**
     * Constant for the parse/format token for hour in am/pm
     * 1-12 (default is '%h').
     *
     */
    TOKEN_H : "%h",

    /**
     * Constant for the parse/format token for hour in day
     * 0-23 (default is '%h24').
     *
     */
    TOKEN_H24 : "%h24",

    /**
     * Constant for the parse/format token for hour in am/pm
     * 01-12 padded to 2 digits (default is '%hh').
     *
     */
    TOKEN_HH : "%hh",

    /**
     * Constant for the parse/format token for hour in day
     * 00-23 padded to 2 digits (default is '%hh24').
     *
     */
    TOKEN_HH24 : "%hh24",

    /**
     * Constant for the parse/format token for a literal
     * representation of the token start (default is '%').
     *
     */
    TOKEN_LITERAL : "%",

    /**
     * Constant for the parse/format token for month in year
     * 1-12 (default is '%m').
     *
     */
    TOKEN_M : "%m",

    /**
     * Constant for the parse/format token for minutes
     * in hour 00-59 padded to 2 digits (default is '%mi').
     *
     */
    TOKEN_MI : "%mi",

    /**
     * Constant for the parse/format token for month in year
     * 01-12 padded to 2 digits (default is '%m').
     *
     */
    TOKEN_MM : "%mm",

    /**
     * Constant for the parse/format token for month in year
     * as an abbreviated string (default is '%mon').
     *
     */
    TOKEN_MON : "%mon",

    /**
     * Constant for the parse/format token for month in year
     * as full string (default is '%month').
     *
     */
    TOKEN_MONTH : "%month",

    /**
     * Constants for the parse/format token for a single
     * space.  While a space is a valid literal, this
     * provides a convenient way to express a space in the
     * CSS classname  (default is '%_').
     *
     */
    TOKEN_SPACE : "%_",

    /**
     * Constant for the parse/format token for seconds in
     * minute 00-59 padded to 2 digits (default is '%ss').
     *
     */
    TOKEN_SS : "%ss",

    /**
     * Constant for the parse/format token for 2 digit year.
     * For parsing, this is interpretted to a full year by
     * adjusting dates to be within 80 years before and 20
     * years after the current year.
     *
     */
    TOKEN_YY : "%yy",

    /**
     * Constant for the parse/format token for full year
     * (default is '%yyyy').
     *
     */
    TOKEN_YYYY : "%yyyy",

    /**
     * Constant for the appropriate order for parse/format
     * tokens to be processed.
     *
     */
    TOKENS : [ "%_",
               "%am",
               "%dayOfWeek",
               "%dow",
               "%dd",
               "%d",
               "%hh24",
               "%h24",
               "%hh",
               "%h",
               "%month",
               "%mon",
               "%mm",
               "%mi",
               "%m",
               "%ss",
               "%yyyy",
               "%yy" ],

    /**
     * Property indicating the expected format of the
     * date.  It may be any one of the following constants:
     * <code>default</code>, <code>short</code>, <code>
     * medium</code>, <code>long</code>, <code>full</code>.
     * The exact result depends on the locale, but
     * generally:
     *
     * <ul>
     *  <li>  The short format is completely numeric, such
     *        as 12.13.52
     *  </li>
     *  <li>  The medium format is longer, such as
     *        Jan 12, 1952
     *  </li>
     *  <li>  The long format is longer, such as
     *        January 12, 1952
     *  </li>
     *  <li>  The full format is pretty completely
     *        specified, such as Tuesday, April 12, 1952 AD
     *  </li>
     * </ul>
     *
     * <p>
     * The default is <code>default</code>... which is
     * equivalent to a medium format.
     * </p>
     *
     */
    format : Dowry.msgs.getMessage("default"),

    /**
     * Property indicating whether case is relevant to
     * pattern matching.  The default is true.
     *
     */
    ignoreCase : true,

    /**
     * Compares the specified values as dates.
     *
     * @param value1  the first value to be compared
     *
     * @param value2  the second value to be compared
     *
     * @return        a negative integer, zero, or a
     *                positive integer indicating that
     *                the first value is less than, equal
     *                to, or greater than the second.
     *
     */
    compare : function(value1, value2)
    {
        arguments[0] = value1.getTime();
        arguments[1] = value2.getTime();

        return this.$super(arguments);
    },

    /**
     * Formats the date into an appropriate string format.
     *
     * <p>
     * This is exposed as a public function, so that it
     * will be easy to override if you require a different
     * date format.
     * </p>
     *
     * @param date  the date
     *
     * @return      the formatted string
     *
     */
    formatDate : function(date)
    {
        var s = null;

        var format = this._getFormat();
        if (date != null &&
            date.getUTCDate != null &&
            format != null)
        {
            var day = date.getDay();
            var dayOfWeek = this.DAYS_OF_WEEK[day];
            var dow = this.DAYS_OF_WEEK_SHORT[day];

            var monthId = date.getMonth();
            var month = this.MONTHS[monthId];
            var mon = this.MONTHS_SHORT[monthId];
            var m = monthId+1;
            var mm = this._toTwoDigits(m);

            var d = date.getDate();
            var dd = this._toTwoDigits(d);

            var h24 = date.getHours();
            var hh24 = this._toTwoDigits(h24);
            var h = h24 > 12 ? h24 - 12 : h24 == 0 ? 12 : h24;
            var hh = this._toTwoDigits(h);

            var yyyy = date.getFullYear();
            var yy = this._toTwoDigits(yyyy);

            var mi = this._toTwoDigits(date.getMinutes());

            var ss = this._toTwoDigits(date.getSeconds());

            var am = this.AM_PM[h24 < 12 ? 0 : 1];

            // format the tokens
            for (var i=0; i < this.TOKENS.length; i++)
            {
                var token = this.TOKENS[i];

                var val = null;
                if (token == this.TOKEN_AM)
                {
                    val = am;
                }
                else if (token == this.TOKEN_D)
                {
                    val = d;
                }
                else if (token == this.TOKEN_DD)
                {
                    val = dd;
                }
                else if (token == this.TOKEN_DAY_OF_WEEK)
                {
                    val = dayOfWeek;
                }
                else if (token == this.TOKEN_DOW)
                {
                    val = dow;
                }
                else if (token == this.TOKEN_H)
                {
                    val = h;
                }
                else if (token == this.TOKEN_H24)
                {
                    val = h24;
                }
                else if (token == this.TOKEN_HH)
                {
                    val = hh;
                }
                else if (token == this.TOKEN_HH24)
                {
                    val = hh24;
                }
                else if (token == this.TOKEN_M)
                {
                    val = m;
                }
                else if (token == this.TOKEN_MI)
                {
                    val = mi;
                }
                else if (token == this.TOKEN_MM)
                {
                    val = mm;
                }
                else if (token == this.TOKEN_MON)
                {
                    val = mon;
                }
                else if (token == this.TOKEN_MONTH)
                {
                    val = month;
                }
                else if (token == this.TOKEN_SPACE)
                {
                    val = " ";
                }
                else if (token == this.TOKEN_SS)
                {
                    val = ss;
                }
                else if (token == this.TOKEN_YY)
                {
                    val = yy;
                }
                else if (token == this.TOKEN_YYYY)
                {
                    val = yyyy;
                }

                format = format.replace(token, val);
            }

            s = format;
        }

        return s;
    },

    /**
     * Parses the string into a date.
     *
     * <p>
     * This is exposed as a public function, so that it
     * will be easy to override if you require a different
     * date parse routine.
     * </p>
     *
     * @param s  the string
     *
     * @return   the parsed date
     *
     */
    parseDate : function(s)
    {
        var val = null;

        var format = this._getFormat();
        if (typeof s == "string" &&
            format != null)
        {
            // converts the format string
            // into tokens
            var tokens = this._getTokens(format);

            // process each parse token
            var dateProps = this._parseTokens(s, tokens);

            // convert the processing results
            // into an actual date
            val = this._buildDate(dateProps);
        }

        return val;
    },

    /**
     * Converts some raw value to a value of the appropriate
     * datatype.
     *
     * @param val  the original value
     *
     * @return     the value converted to a date
     *
     */
    toDatatype : function(val)
    {
        if (val != null &&
            val.getUTCDate == null)
        {
            val = val.toString();
            val = this.parseDate(val);
        }

        return val;
    },

    /**
     * Converts some value of the appropriate datatype to
     * a string representation.
     *
     * @param val  the original value
     *
     * @return     the value converted to a string
     *
     */
    toFormattedString : function(val)
    {
        var s = null;

        // number to date
        if (typeof val == "number")
        {
            val = new Date(val);
        }

        // anything else, try to parse it
        if (val != null &&
            val.getUTCDate == null)
        {
            val = val.toString();
            val = this.parseDate(val);
        }

        if(val != null &&
           val.getUTCDate != null)
        {
            // format the string
            s = this.formatDate(val);
        }

        return s;
    },

    /**
     * Validates that:
     *
     * <ol>
     *  <li>  the value is provided, if required as
     *        specified by map key <code>required</code>
     *  </li>
     *  <li>  the value represents a valid date
     *  </li>
     * </ol>
     *
     * @param val    a value representation to be validated
     *
     * @return       the string error message, if the
     *               input is invalid; otherwise, null
     *
     */
    validate : function(val)
    {
        var msg = this.$super(arguments);

        // can only be an invalid date if it
        // is not already a date object
        if (msg == null &&
            val != null &&
            val.getUTCDate == null)
        {
            val = this.toDatatype(val);
            if (val == null ||
                val.getUTCDate == null)
            {
                msg = this.ERROR_INVALID;
            }
        }

        return msg;
    },

    /**
     * Builds a date from the date properties map.
     *
     * @param dateProps  the date properties map
     *
     * @return           the date object
     *
     */
    _buildDate : function(dateProps)
    {
        var d = null;

        // interpret our formatted date fields
        if (dateProps != null)
        {
            var year = dateProps.year;
            var month = dateProps.month;
            var date = dateProps.date;
            var hour24 = dateProps.hour24;
            var hour = dateProps.hour;
            var minute = dateProps.minute;
            var second = dateProps.second;
            var am = dateProps.am;

            if (hour24 == null &&
                hour != null)
            {
                if (hour == 12)
                {
                    hour = 0;
                }

                if (am == 1)
                {
                    hour24 = hour + 12;
                }
                else
                {
                    hour24 = hour;
                }
            }

            // TODO: as long as it matches the format
            // assume anything else not provided
            // is 0?
            if (year == null)
            {
                year = 0;
            }
            if (month == null)
            {
                month = 0;
            }
            if (date == null)
            {
                date = 0;
            }
            if (hour24 == null)
            {
                hour24 = 0;
            }
            if (minute == null)
            {
                minute = 0;
            }
            if (second == null)
            {
                second = 0;
            }

            // TODO: what about time zone?
            d = new Date(year, month, date, hour24, minute, second);

            // make sure that we haven't changed
            // the date as part of parsing... this
            // is to avoid any problems with bogus
            // dates like 'Feb 31, 2006' being silently
            // converted to 'Mar 3, 2006' or whatnot
            if (d != null &&
                (year != d.getFullYear() ||
                 month != d.getMonth() ||
                 date != d.getDate()))
            {
                d = null;
            }
        }

        return d;
    },

    /**
     * Gets the format accounting for any literal parse/
     * format mask.
     *
     * @return  the parse/format mask
     *
     */
    _getFormat : function()
    {
        var f = this.format;

        if (f == null ||
            f == this.FORMAT_DEFAULT ||
            f == this.FORMAT_MEDIUM)
        {
            f = this.MEDIUM;
        }
        if (f == this.FORMAT_SHORT)
        {
            f = this.SHORT;
        }
        else if (f == this.FORMAT_LONG)
        {
            f = this.LONG;
        }
        else if (f == this.FORMAT_FULL)
        {
            f = this.FULL;
        }

        return f;
    },

    /**
     * Gets the tokens associated with the parse/format
     * mask.
     *
     * @param format  the formatting string
     *
     * @return        the parse/format mask
     *
     */
    _getTokens : function(format)
    {
        // convert format into tokens
        var tokens = new Array();

        // use start to indicate the start
        // of a new token
        var start = 0;
        for (var i=0; i < format.length; i++)
        {
            var c = format.charAt(i);

            // character represents the start of
            // a token
            if (c == this.TOKEN_LITERAL)
            {
                // add any literal text between
                // last token and this token...
                if (start < i)
                {
                    var s = format.substring(start, i);
                    if (this.ignoreCase)
                    {
                        s = s.toLowerCase();
                    }

                    tokens[tokens.length] = s;
                }

                // loop through all explicit tokens
                for (var j=0; j < this.TOKENS.length; j++)
                {
                    var token = this.TOKENS[j];
                    if (format.indexOf(token, i) == i)
                    {
                        var func = null;
                        if (token == this.TOKEN_AM)
                        {
                            func = this._parseAm;
                        }
                        else if (token == this.TOKEN_D)
                        {
                            func = this._parseD;
                        }
                        else if (token == this.TOKEN_DD)
                        {
                            func = this._parseDd;
                        }
                        else if (token == this.TOKEN_DAY_OF_WEEK)
                        {
                            func = this._parseDayOfWeek;
                        }
                        else if (token == this.TOKEN_DOW)
                        {
                            func = this._parseDow;
                        }
                        else if (token == this.TOKEN_H)
                        {
                            func = this._parseH;
                        }
                        else if (token == this.TOKEN_H24)
                        {
                            func = this._parseH24;
                        }
                        else if (token == this.TOKEN_HH)
                        {
                            func = this._parseHh;
                        }
                        else if (token == this.TOKEN_HH24)
                        {
                            func = this._parseHh24;
                        }
                        else if (token == this.TOKEN_M)
                        {
                            func = this._parseM;
                        }
                        else if (token == this.TOKEN_MI)
                        {
                            func = this._parseMi;
                        }
                        else if (token == this.TOKEN_MM)
                        {
                            func = this._parseMm;
                        }
                        else if (token == this.TOKEN_MON)
                        {
                            func = this._parseMon;
                        }
                        else if (token == this.TOKEN_MONTH)
                        {
                            func = this._parseMonth;
                        }
                        else if (token == this.TOKEN_SPACE)
                        {
                            tokens[tokens.length] = " ";
                        }
                        else if (token == this.TOKEN_SS)
                        {
                            func = this._parseSs;
                        }
                        else if (token == this.TOKEN_YY)
                        {
                            func = this._parseYy;
                        }
                        else if (token == this.TOKEN_YYYY)
                        {
                            func = this._parseYyyy;
                        }
                        else
                        {
                            tokens[tokens.length] = this.TOKEN_LITERAL;
                        }

                        if (typeof func == "function")
                        {
                            tokens[tokens.length] = func;
                            i = i + token.length - 1;
                            break;
                        }
                    }
                }

                // advance the start token so
                // it matches our counter when
                // we begin the next iteration
                start = i+1;
            }
        }

        // any remainder represents a final
        // literal token...
        if (start < format.length)
        {
            var s = format.substring(start);
            if (this.ignoreCase)
            {
                s = s.toLowerCase();
            }

            tokens[tokens.length] = s;
        }

        return tokens;
    },

    /**
     * Parses out the string as a 2 digit number.
     *
     * @param s          the string representation
     *
     * @param start      the index of the start of token
     *
     * @param dateProps  map of date properties (this will
     *                   be updated as part of the parsing)
     *
     * @param propName   the property name to be stored on
     *                   the date properties map
     *
     * @param minVal     the minimum integer that the parsed
     *                   value must be equal or greater to
     *
     * @param maxVal     the maximum integer that the parsed
     *                   value must be equal or less than
     *
     * @return           the updated index after the token
     *
     */
    _parse2Digits : function(s,
                             start,
                             dateProps,
                             propName,
                             minVal,
                             maxVal)
    {
        var found = false;
        var s = s.substring(start, start+2);
        if (s.length == 2)
        {
            var i = Number(s);
            if (i >= minVal && i <= maxVal)
            {
                dateProps[propName] = i;
                start = start + 2;
                found = true;
            }
        }

        if (!found)
        {
            start = -1;
        }

        return start;
    },

    /**
     * Parses out the am/pm from the string representation.
     *
     * @param s          the string representation
     *
     * @param start      the index of the start of token
     *
     * @param dateProps  map of date properties (this will
     *                   be updated as part of the parsing)
     *
     * @return           the updated index after the token
     *
     */
    _parseAm : function(s, start, dateProps)
    {
        var found = false;
        for (var i=0; i < this.AM_PM.length; i++)
        {
            var am = this.AM_PM[i];
            if (this.ignoreCase)
            {
                am = am.toLowerCase();
            }

            if (s.indexOf(am, start) == start)
            {
                dateProps.am = i;
                start = start + am.length;
                found = true;
                break;
            }
        }

        if (!found)
        {
            start = -1;
        }

        return start;
    },

    /**
     * Parses out the day of week from the string
     * representation.
     *
     * @param s          the string representation
     *
     * @param start      the index of the start of token
     *
     * @param dateProps  map of date properties (this will
     *                   be updated as part of the parsing)
     *
     * @return           the updated index after the token
     *
     */
    _parseDayOfWeek : function(s, start, dateProps)
    {
        var found = false;
        for (var i=0; i < this.DAYS_OF_WEEK.length; i++)
        {
            var dayOfWeek = this.DAYS_OF_WEEK[i];
            if (this.ignoreCase)
            {
                dayOfWeek = dayOfWeek.toLowerCase();
            }

            if (s.indexOf(dayOfWeek, start) == start)
            {
                dateProps.dayOfWeek = i;
                start = start + dayOfWeek.length;
                found = true;
                break;
            }
        }

        if (!found)
        {
            start = -1;
        }

        return start;
    },

    /**
     * Parses out the day of month from the string
     * representation.
     *
     * @param s          the string representation
     *
     * @param start      the index of the start of token
     *
     * @param dateProps  map of date properties (this will
     *                   be updated as part of the parsing)
     *
     * @param nextToken  the next token to be parsed (may be
     *                   used to delimit the end of the
     *                   current token)
     *
     * @return           the updated index after the token
     *
     */
    _parseD : function(s, start, dateProps, nextToken)
    {
        return this._parseToToken(s, start, dateProps, "date", 1, 31, nextToken);
    },

    /**
     * Parses out the day of month from the string
     * representation.
     *
     * @param s          the string representation
     *
     * @param start      the index of the start of token
     *
     * @param dateProps  map of date properties (this will
     *                   be updated as part of the parsing)
     *
     * @return           the updated index after the token
     *
     */
    _parseDd : function(s, start, dateProps)
    {
        return this._parse2Digits(s, start, dateProps, "date", 1, 31);
    },

    /**
     * Parses out the day of week from the string
     * representation.
     *
     * @param s          the string representation
     *
     * @param start      the index of the start of token
     *
     * @param dateProps  map of date properties (this will
     *                   be updated as part of the parsing)
     *
     * @return           the updated index after the token
     *
     */
    _parseDow : function(s, start, dateProps)
    {
        var found = false;
        for (var i=0; i < this.DAYS_OF_WEEK_SHORT.length; i++)
        {
            var dayOfWeek = this.DAYS_OF_WEEK_SHORT[i];
            if (this.ignoreCase)
            {
                dayOfWeek = dayOfWeek.toLowerCase();
            }

            if (s.indexOf(dayOfWeek, start) == start)
            {
                dateProps.dayOfWeek = i;
                start = start + dayOfWeek.length;
                found = true;
                break;
            }
        }

        if (!found)
        {
            start = -1;
        }

        return start;
    },

    /**
     * Parses out the hour of am/pm from the string
     * representation.
     *
     * @param s          the string representation
     *
     * @param start      the index of the start of token
     *
     * @param dateProps  map of date properties (this will
     *                   be updated as part of the parsing)
     *
     * @param nextToken  the next token to be parsed (may be
     *                   used to delimit the end of the
     *                   current token)
     *
     * @return           the updated index after the token
     *
     */
    _parseH : function(s, start, dateProps, nextToken)
    {
        return this._parseToToken(s, start, dateProps, "hour", 1, 12, nextToken);
    },

    /**
     * Parses out the hour of day from the string
     * representation.
     *
     * @param s          the string representation
     *
     * @param start      the index of the start of token
     *
     * @param dateProps  map of date properties (this will
     *                   be updated as part of the parsing)
     *
     * @param nextToken  the next token to be parsed (may be
     *                   used to delimit the end of the
     *                   current token)
     *
     * @return           the updated index after the token
     *
     */
    _parseH24 : function(s, start, dateProps, nextToken)
    {
        return this._parseToToken(s, start, dateProps, "hour24", 0, 23, nextToken);
    },

    /**
     * Parses out the hour of am/pm from the string
     * representation.
     *
     * @param s          the string representation
     *
     * @param start      the index of the start of token
     *
     * @param dateProps  map of date properties (this will
     *                   be updated as part of the parsing)
     *
     * @return           the updated index after the token
     *
     */
    _parseHh : function(s, start, dateProps)
    {
        return this._parse2Digits(s, start, dateProps, "hour", 1, 12);
    },

    /**
     * Parses out the hour of day from the string
     * representation.
     *
     * @param s          the string representation
     *
     * @param start      the index of the start of token
     *
     * @param dateProps  map of date properties (this will
     *                   be updated as part of the parsing)
     *
     * @return           the updated index after the token
     *
     */
    _parseHh24 : function(s, start, dateProps)
    {
        return this._parse2Digits(s, start, dateProps, "hour24", 0, 23);
    },

    /**
     * Parses out the month from the string representation.
     *
     * @param s          the string representation
     *
     * @param start      the index of the start of token
     *
     * @param dateProps  map of date properties (this will
     *                   be updated as part of the parsing)
     *
     * @param nextToken  the next token to be parsed (may be
     *                   used to delimit the end of the
     *                   current token)
     *
     * @return           the updated index after the token
     *
     */
    _parseM : function(s, start, dateProps, nextToken)
    {
        var rc = this._parseToToken(s, start, dateProps, "month", 1, 12, nextToken);
        if (rc > -1 && dateProps.month != null)
        {
            dateProps.month = dateProps.month - 1;
        }
        return rc;
    },

    /**
     * Parses out the month from the string representation.
     *
     * @param s          the string representation
     *
     * @param start      the index of the start of token
     *
     * @param dateProps  map of date properties (this will
     *                   be updated as part of the parsing)
     *
     * @return           the updated index after the token
     *
     */
    _parseMm : function(s, start, dateProps)
    {
        var rc = this._parse2Digits(s, start, dateProps, "month", 1, 12);

        if (rc > -1 && dateProps.month != null)
        {
            dateProps.month = dateProps.month - 1;
        }

        return rc;
    },

    /**
     * Parses out the month from the string representation.
     *
     * @param s          the string representation
     *
     * @param start      the index of the start of token
     *
     * @param dateProps  map of date properties (this will
     *                   be updated as part of the parsing)
     *
     * @return           the updated index after the token
     *
     */
    _parseMon : function(s, start, dateProps)
    {
        var found = false;
        for (var i=0; i < this.MONTHS_SHORT.length; i++)
        {
            var month = this.MONTHS_SHORT[i];
            if (this.ignoreCase)
            {
                month = month.toLowerCase();
            }

            if (s.indexOf(month, start) == start)
            {
                dateProps.month = i;
                start = start + month.length;
                found = true;
                break;
            }
        }

        if (!found)
        {
            start = -1;
        }

        return start;
    },

    /**
     * Parses out the month from the string representation.
     *
     * @param s          the string representation
     *
     * @param start      the index of the start of token
     *
     * @param dateProps  map of date properties (this will
     *                   be updated as part of the parsing)
     *
     * @return           the updated index after the token
     *
     */
    _parseMonth : function(s, start, dateProps)
    {
        var found = false;
        for (var i=0; i < this.MONTHS.length; i++)
        {
            var month = this.MONTHS[i];
            if (this.ignoreCase)
            {
                month = month.toLowerCase();
            }

            if (s.indexOf(month, start) == start)
            {
                dateProps.month = i;
                start = start + month.length;
                found = true;
                break;
            }
        }

        if (!found)
        {
            start = -1;
        }

        return start;
    },

    /**
     * Parses out the minutes of hour from the string
     * representation.
     *
     * @param s          the string representation
     *
     * @param start      the index of the start of token
     *
     * @param dateProps  map of date properties (this will
     *                   be updated as part of the parsing)
     *
     * @return           the updated index after the token
     *
     */
    _parseMi : function(s, start, dateProps)
    {
        return this._parse2Digits(s, start, dateProps, "minute", 0, 59);
    },

    /**
     * Parses out the seconds of minute from the string
     * representation.
     *
     * @param s          the string representation
     *
     * @param start      the index of the start of token
     *
     * @param dateProps  map of date properties (this will
     *                   be updated as part of the parsing)
     *
     * @return           the updated index after the token
     *
     */
    _parseSs : function(s, start, dateProps)
    {
        return this._parse2Digits(s, start, dateProps, "second", 0, 59);
    },

    /**
     * Parses out the string until the start of the next
     * token.  If we can't figure out the start of the next
     * token (because its not a string literal, typically),
     * we'll assume that the token is 2 digits.
     *
     * @param s          the string representation
     *
     * @param start      the index of the start of token
     *
     * @param dateProps  map of date properties (this will
     *                   be updated as part of the parsing)
     *
     * @param propName   the property name to be stored on
     *                   the date properties map
     *
     * @param minVal     the minimum integer that the parsed
     *                   value must be equal or greater to
     *
     * @param maxVal     the maximum integer that the parsed
     *                   value must be equal or less than
     *
     * @param nextToken  the next token to be parsed (may be
     *                   used to delimit the end of the
     *                   current token)
     *
     * @return           the updated index after the token
     *
     */
    _parseToToken : function(s,
                             start,
                             dateProps,
                             propName,
                             minVal,
                             maxVal,
                             nextToken)
    {
        if (nextToken == null ||
            typeof nextToken == "string")
        {
            var found = false;

            var end = s.length;
            if (typeof nextToken == "string")
            {
                end = s.indexOf(nextToken, start);
            }

            if (end > -1)
            {
                s = s.substring(start, end);
                if (s.length > 0 && s.length < 3)
                {
                    var i = Number(s);
                    if (i >= minVal && i <= maxVal)
                    {
                        dateProps[propName] = i;
                        start = end;
                        found = true;
                    }
                }

                if (!found)
                {
                    start = -1;
                }
            }
        }
        else
        {
            start = this._parse2Digits(s,
                                       start,
                                       dateProps,
                                       propName,
                                       minVal,
                                       maxVal);
        }

        return start;
    },

    /**
     * Parses the string value based on the array of parse
     * tokens.
     *
     * @param s       the string to be parsed
     *
     * @param tokens  the array of parse tokens representing
     *                the parse mask
     *
     * @return        a map of date properties
     *
     */
    _parseTokens : function(s, tokens)
    {
        var dateProps = null;

        if (s != null &&
            tokens != null)
        {
            if (this.ignoreCase)
            {
                s = s.toLowerCase();
            }

            var start = 0;
            dateProps = new Object();
            for (var i=0; i < tokens.length; i++)
            {
                var token = tokens[i];

                // string literal
                if (typeof token == "string")
                {
                    if (s.indexOf(token, start) == start)
                    {
                        start = start + token.length;
                    }
                    else
                    {
                        dateProps = null;
                        break;
                    }
                }

                // parse function
                else if (typeof token == "function")
                {
                    var nextToken = tokens[i+1];

                    var rc = token.call(this,
                                        s,
                                        start,
                                        dateProps,
                                        nextToken);

                    if (rc > start)
                    {
                        start = rc;
                    }
                    else
                    {
                        dateProps = null;
                        break;
                    }
                }
            }

            // if we still have text left in our
            // string to be parsed, then it must
            // not match our mask...
            if (start != s.length)
            {
                dateProps = null;
            }
        }

        return dateProps;
    },

    /**
     * Parses out the 2 digit year from the string
     * representation.
     *
     * @param s          the string representation
     *
     * @param start      the index of the start of token
     *
     * @param dateProps  map of date properties (this will
     *                   be updated as part of the parsing)
     *
     * @return           the updated index after the token
     *
     */
    _parseYy : function(s, start, dateProps)
    {
        var rc = this._parse2Digits(s, start, dateProps, "year", 0, 99);
        if (rc > -1 && dateProps.year != null)
        {
            var currentYear = (new Date()).getFullYear();
            var century = Math.round(currentYear / 100) * 100;
            if (dateProps.year + century <= currentYear + 20 &&
                dateProps.year + century > currentYear - 80)
            {
                dateProps.year = dateProps.year + century;
            }
            else if (dateProps.year + century + 100 <= currentYear + 20 &&
                     dateProps.year + century + 100 > currentYear - 80)
            {
                dateProps.year = dateProps.year + century + 100;
            }
            else
            {
                dateProps.year = dateProps.year + century - 100;
            }
        }
        return rc;
    },

    /**
     * Parses out the full year from the string
     * representation.
     *
     * @param s          the string representation
     *
     * @param start      the index of the start of token
     *
     * @param dateProps  map of date properties (this will
     *                   be updated as part of the parsing)
     *
     * @return           the updated index after the token
     *
     */
    _parseYyyy : function(s, start, dateProps)
    {
        var found = false;
        var s = s.substring(start, start+4);
        if (s.length == 4)
        {
            var i = Number(s);
            if (i >= 1000)
            {
                dateProps.year = i;
                start = start + 4;
                found = true;
            }
        }

        if (!found)
        {
            start = -1;
        }

        return start;
    },

    /**
     * Converts a number to 2 digits, padding it if it is
     * shorter or truncating all but the final 2 digits, if
     * it is too large.
     *
     * @param n  the number
     *
     * @return   the number as 2 digits
     *
     */
    _toTwoDigits : function(n)
    {
        if (typeof n == "number")
        {
            n = n.toString();
            if (n.length == 1)
            {
                n = "0" + n;
            }
            else if (n.length > 2)
            {
                n = n.substring(n.length - 2);
            }
        }

        return n;
    }
});